using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace PhpDocBlockGenerator.Model.Php
{
    /// <summary>
    /// possible types a doc block property can be
    /// </summary>
    public enum DocBlockPropertyTypes
    {
        Misc,
        Api,
        Author,
        Copyright,
        Deprecated,
        Description,
        Example,
        Filesource,
        Global,
        Ignore,
        InheritDoc,
        Internal,
        License,
        Link,
        Method,
        Package,
        Param,
        Property,
        Return,
        See,
        Since,
        Source,
        SubPackage,
        Throws,
        Todo,
        Uses,
        Var,
        Version,
    }

    /// <summary>
    /// base property (@x) of a doc block
    /// </summary>
    public class DocBlockProperty
    {
        /// <summary>
        /// constant standard text values
        /// </summary>
        public const String AuthorStandardText = "Generated by phpDocBlockGen (C) Mario Sehmisch und Daniel Rau";
        public const String AuthorUnisterText = "Unister GmbH <teamleitung-dev@unister-gmbh.de>";
        public const String VersionStandardText = "$Id:$";

        /// <summary>
        /// the description of the doc block property
        /// </summary>
        public string Description { get; set; }

        /// <summary>
        /// the description of the doc block property as a list
        /// </summary>
        public List<string> DescriptionList { get; set; }

        public DocBlockPropertyTypes ObjectType { get; set; }

        /// <summary>
        /// if the type is not in the propertytype enum
        /// </summary>
        private String _typeName = String.Empty;
        public String TypeName
        {
            get { return ObjectType == DocBlockPropertyTypes.Misc ? _typeName : ObjectType.ToString(); }
            set { _typeName = value; }
        }

        /// <summary>
        /// container of options
        /// </summary>
        public Options Options { get; private set; }

        /// <summary>
        /// ctor
        /// </summary>
        public DocBlockProperty(Options options)
        {
            DescriptionList = null;
            Options = options;
        }

        /// <summary>
        /// copy ctor
        /// </summary>
        /// <param name="other">object to copy</param>
        /// <param name="options"></param>
        public DocBlockProperty(DocBlockProperty other, Options options)
            : this(options)
        {
            ObjectType = other.ObjectType;
            Description = other.Description;
        }

        /// <summary>
        /// checks if a doc block is null or wont write minimum one line
        /// </summary>
        /// <param name="prop">the property that shoul be checked</param>
        /// <returns></returns>
        public static bool IsNullOrEmpty(DocBlockProperty prop)
        {
            return prop == null || prop.IsEmpty();
        }

        /// <summary>
        /// check if the property is empty
        /// </summary>
        /// <returns></returns>
        public virtual bool IsEmpty()
        {
            // category and subpackage are deprecated
            return TypeName.ToLower().Equals("category") || TypeName.ToLower().Equals("subpackage");
        }

        /// <summary>
        /// Makes a list from a Multiline-String
        /// </summary>
        public void CreateDescriptionList()
        {
            String[] lines = Description.Split(new[] {"\n", "\r", "\r\n"}, StringSplitOptions.None);
            for (var i = 0; i < lines.Length; ++i)
            {
                lines[i] = lines[i].Trim();
            }
            DescriptionList = new List<String>(lines);
        }

        /// <summary>
        /// parses a concated string of file lines to extract property type
        /// and description
        /// </summary>
        /// <param name="lines"></param>
        public void Parse(String lines)
        {
            //if lines exist
            if (!String.IsNullOrEmpty(lines))
            {
                //match for type of the property
                MatchCollection regObjectTypeResult = PhpRegex.RegSearchFirstWordForType.Matches(lines);

                //if type exists
                if (regObjectTypeResult.Count > 0)
                {
                    var enumNames = Enum.GetNames(typeof(DocBlockPropertyTypes));

                    //compare type to all possible property types and save enum value
                    for (int i = 0; i < enumNames.Length; ++i)
                    {
                        if (String.Equals(enumNames[i].ToLower(), regObjectTypeResult[0].Value))
                        {
                            ObjectType = (DocBlockPropertyTypes)i;
                            break;
                        }
                    }

                    if (ObjectType == DocBlockPropertyTypes.Misc)
                    {
                        TypeName = regObjectTypeResult[0].Value;
                    }
                }
                
                //extract all remaining text after the matched type
                MatchCollection regObjectDescResult = PhpRegex.RegSearchAfterFirstWordForDesc.Matches(lines);

                //if there is any text after matched type
                if (regObjectDescResult.Count > 0)
                {
                    //concat all matches to one string ...
                    String temp = regObjectDescResult.Cast<Match>().Aggregate(String.Empty, (current, subMatch) => current + (subMatch.Value.Trim() + " "));

                    //and save it
                    Description = temp;
                }
            }
        }

        /// <summary>
        /// returns the indenting
        /// </summary>
        /// <returns>the indenting needed for this type of param</returns>
        public virtual int GetDescriptionIndent()
        {
            return 0;
        }

        /// <summary>
        /// Creates a List of Strings whicht represents the lines of this property
        /// </summary>
        /// <param name="indent">the indent before the docblock begins</param>
        /// <param name="indents">an array of indents for several purposes</param>
        /// <returns>the lines of the property</returns>
        public virtual List<String> ToStringList(String indent, int[] indents)
        {
            var list = new List<String>();
            String line = indent + " * @" + TypeName.ToLower().PadRight(indents[0]);
            _AddDescription(ref list, line, indent, indents);
            return list;
        }

        /// <summary>
        /// Adds the lines including the description
        /// </summary>
        /// <param name="lines"></param>
        /// <param name="line">the line before the description</param>
        /// <param name="indent">the indent before the docblock begins</param>
        /// <param name="indents">the lines of the property</param>
        protected void _AddDescription(ref List<String> lines, String line , String indent, int[] indents)
        {
            if (Description == null)
                lines.Add(line);
            else
            {
                // first create a temporary list
                var tempLines = new List<String>();
                // split the description with the linebreaks
                String[] description = Description.Split(new[] { "\n", "\r", "\r\n" }, StringSplitOptions.None);
                // add first line
                tempLines.Add(line + " " + description[0].Trim());
                // add other lines with right indenting
                for (int i = 1; i < description.Length; ++i)
                {
                    tempLines.Add(indent + " * " + "".PadRight(indents[0] + 2) + description[i].Trim());
                }

                // temp bool which indicates whether there is minimum of one line which is too long or not
                bool longLineFound = (from s in tempLines 
                                      let tabCount = s.Split(new[] {"\t"}, StringSplitOptions.None).Length - 1 
                                      select s.Length + (tabCount > 0 ? tabCount*3 : 0)).Any(length => length > Options.MaxColumnNumber);

                if (Options.IsMaxColumnNumberUsed && longLineFound)
                {
                    // first clear the templines
                    tempLines.Clear();
                    // create a "starting line"
                    String currentLine = line + " ";
                    // make a list of all dividable parts of the description (mostly words)
                    List<String> splitList = String.Join(" ", description).Trim().Split(new[] { " " }, StringSplitOptions.None).ToList();

                    // add all "words" to lines, which are not too long
                    while (splitList.Count != 0)
                    {
                        int tabCount = currentLine.Split(new[] {"\t"}, StringSplitOptions.None).Length - 1;
                        // first word needs no space
                        bool first = true;

                        // add words
                        do
                        {
                            // check if it is the first word
                            if (first)
                                first = false;
                            else
                                currentLine += " ";
                            // add word
                            currentLine += splitList.First();
                            // remove it from list
                            splitList.Remove(splitList.First());
                        } while (splitList.Count > 0 && currentLine.Length + splitList.First().Length + tabCount*3 + 1 <= 120);

                        // add the created line to the list
                        tempLines.Add(currentLine);
                        // create a new temporary line
                        currentLine = indent + " * " + "".PadRight(indents[0] + 2);
                    }
                }

                // add temporary lines to the referenced list of lines
                lines.AddRange(tempLines);
            }
        }


        /// <summary>
        /// removes a specific word (via regex result) from the description string
        /// </summary>
        /// <param name="word">word to remove (extracted from a regex result)</param>
        protected void _RemoveFromDescription(Match word)
        {
            Description = Description.Remove(word.Index, word.Length);
            Description = Description.Trim();
        }
    }

    /// <summary>
    /// equalitity comparer for property objects
    /// </summary>
    public class DocBlockPropertyComparer : EqualityComparer<DocBlockProperty>
    {
        /// <summary>
        /// compares the descriptions for equalitity
        /// </summary>
        /// <param name="x">first doc block property to compare</param>
        /// <param name="y">second doc block property to compare</param>
        /// <returns>true if descriptions are equal, false otherwise</returns>
        public override bool Equals(DocBlockProperty x, DocBlockProperty y)
        {
            if (x.Description.Equals(y.Description))
                return true;
            return false;
        }

        /// <summary>
        /// has no functionality: returns simply 0 as a pseudo hashcode
        /// </summary>
        /// <param name="obj">property to compute the hashcode from</param>
        /// <returns>0</returns>
        public override int GetHashCode(DocBlockProperty obj)
        {
            return 0;
        }
    }

    /// <summary>
    /// the base class for property variables, which provides methods and properties for the
    /// param and the return-class
    /// </summary>
    public class DocBlockPropertyVariable : DocBlockProperty
    {
        /// <summary>
        /// the type of the variable (and its subtypes)
        /// </summary>
        private PhpDataType _variableDataType = new PhpDataType();
        public PhpDataType VariableDataType
        {
            get { return _variableDataType; }
            set { _variableDataType = value; }
        }

        /// <summary>
        /// copy ctor
        /// </summary>
        /// <param name="other">object to copy</param>
        /// <param name="options"></param>
        public DocBlockPropertyVariable(DocBlockProperty other, Options options)
            : base(other, options)
        {
            // ReSharper disable DoNotCallOverridableMethodsInConstructor
            Parse();
            // ReSharper restore DoNotCallOverridableMethodsInConstructor
        }

        /// <summary>
        /// ctor
        /// </summary>
        public DocBlockPropertyVariable(Options options) : base(options) { }


        /// <summary>
        /// parses the description and extracts the first and the second word
        /// and matches it to several data-types
        /// </summary>
        public virtual void Parse()
        {
            // check if description is null
            if (Description == null)
                return;

            //match all words in description
            MatchCollection regFirstLastWordResult = PhpRegex.RegSearchWords.Matches(Description);

            if (regFirstLastWordResult.Count > 0)
            {
                VariableDataType.SetTypeFromString(regFirstLastWordResult[0].Value);
                _RemoveFromDescription(regFirstLastWordResult[0]);
            }
        }

        public override int GetDescriptionIndent()
        {
            return VariableDataType.ToString().Length + 1;
        }

        /// <summary>
        /// Creates a List of Strings whicht represents the lines of this property
        /// </summary>
        /// <param name="indent">the indent before the docblock begins</param>
        /// <param name="indents">an array of indents for several purposes</param>
        /// <returns>the lines of the property</returns>
        public override List<String> ToStringList(String indent, int[] indents)
        {
            var list = new List<String>();
            String line = indent + " * @"
                          + (TypeName.ToLower().PadRight(indents[1])
                             + " " + VariableDataType).PadRight(indents[0]);
            _AddDescription(ref list, line, indent, indents);
            return list;
        }
    }

    /// <summary>
    /// the class for all returns in the docblock
    /// </summary>
    public class DocBlockPropertyReturn : DocBlockPropertyVariable
    {
        /// <summary>
        /// copy ctor
        /// </summary>
        /// <param name="other">object to copy</param>
        /// <param name="options"></param>
        public DocBlockPropertyReturn(DocBlockProperty other, Options options)
            : base(other, options)
                {}

        /// <summary>
        /// Ctor
        /// </summary>
        public DocBlockPropertyReturn(Options options) : base(options) 
        {
            ObjectType = DocBlockPropertyTypes.Return;
        }

        /// <summary>
        /// check if the property is empty
        /// </summary>
        /// <returns></returns>
        public override bool IsEmpty()
        {
            return VariableDataType.Type == PhpDataTypes.None;
        }
    }

    /// <summary>
    /// the class for all params in the doc block
    /// </summary>
    public class DocBlockPropertyParam : DocBlockPropertyVariable
    {
        /// <summary>
        /// the name of the variable
        /// </summary>
        private String _variableName = String.Empty;
        public String VariableName
        {
            get { return _variableName; }
            set { _variableName = value; }
        }

        /// <summary>
        /// copy ctor
        /// </summary>
        /// <param name="other">object to copy</param>
        /// <param name="options"></param>
        public DocBlockPropertyParam(DocBlockProperty other, Options options)
            : base(other, options)
        {}

        /// <summary>
        /// ctor
        /// </summary>
        public DocBlockPropertyParam(Options options) : base(options)
        {
            ObjectType = DocBlockPropertyTypes.Param;
        }


        /// <summary>
        /// parses the description for the variable name of the param-property and
        /// removes it from the description when found
        /// </summary>
        public override void Parse()
        {
            // check if description is null
            if (Description == null)
                return;

            //match all words in description
            MatchCollection regFirstLastWordResult = PhpRegex.RegSearchWords.Matches(Description);

            if (regFirstLastWordResult.Count > 0 && PhpRegex.RegSearchVarName.IsMatch(regFirstLastWordResult[0].Value))
            {
                VariableName = regFirstLastWordResult[0].Value;
                _RemoveFromDescription(regFirstLastWordResult[0]);
            }
            else if (regFirstLastWordResult.Count > 1 && PhpRegex.RegSearchVarName.IsMatch(regFirstLastWordResult[1].Value))
            {
                VariableName = regFirstLastWordResult[1].Value;
                VariableDataType.SetTypeFromString(regFirstLastWordResult[0].Value);
                _RemoveFromDescription(regFirstLastWordResult[1]);
                _RemoveFromDescription(regFirstLastWordResult[0]);
            }
        }

        public override int GetDescriptionIndent()
        {
            return GetDescriptionIndent(0);
        }

        public int GetDescriptionIndent(int longestIndent)
        {
            int indent = base.GetDescriptionIndent();
            if (longestIndent > indent)
                indent = longestIndent;
            if (VariableName != null)
                indent += VariableName.Length + 1;
            return indent;
        }
        public int GetTypeIndent()
        {
            return base.GetDescriptionIndent();
        }

        /// <summary>
        /// Creates a List of Strings whicht represents the lines of this property
        /// </summary>
        /// <param name="indent">the indent before the docblock begins</param>
        /// <param name="indents">an array of indents for several purposes</param>
        /// <returns>the lines of the property</returns>
        public override List<String> ToStringList(String indent, int[] indents)
        {
            var list = new List<String>();
            String line = indent + " * @" +
                          (
                              (TypeName.ToLower().PadRight(indents[1])
                              + " " + VariableDataType).PadRight(indents[2])
                             + " " + VariableName).PadRight(indents[0]);
            _AddDescription(ref list, line, indent, indents);
            return list;
        }
    }

    /// <summary>
    /// doc block property for exceptions
    /// </summary>
    public class DocBlockPropertyException : DocBlockPropertyReturn
    {
        /// <summary>
        /// copy ctor
        /// </summary>
        /// <param name="other">object to copy</param>
        /// <param name="options"></param>
        public DocBlockPropertyException(DocBlockProperty other, Options options)
            : base(other, options)
        {}

        /// <summary>
        /// ctor
        /// </summary>
        public DocBlockPropertyException(Options options) : base(options)
        {
            ObjectType = DocBlockPropertyTypes.Throws;
        }
    }

    /// <summary>
    /// equalitity comparer for exception property objects
    /// </summary>
    public class DocBlockPropertyExceptionComparer : EqualityComparer<DocBlockPropertyException>
    {
        /// <summary>
        /// compares the exception names for equalitity
        /// </summary>
        /// <param name="x">first doc block exception property to compare</param>
        /// <param name="y">second doc block exception property to compare</param>
        /// <returns>true if names are equal, false otherwise</returns>
        public override bool Equals(DocBlockPropertyException x, DocBlockPropertyException y)
        {
            if (x.VariableDataType.Equals(y.VariableDataType))
                return true;
            return false;
        }

        /// <summary>
        /// has no functionality: returns simply 0 as a pseudo hashcode
        /// </summary>
        /// <param name="obj">property to compute the hashcode from</param>
        /// <returns>0</returns>
        public override int GetHashCode(DocBlockPropertyException obj)
        {
            return 0;
        }
    }
}
